| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- <template>
- <div>
- <div class="inner--headers">
- <h2>인플루언서 프로필</h2>
- <div class="bread--crumbs--wrap">
- <span>홈</span>
- <span>인플루언서</span>
- <span>프로필</span>
- </div>
- </div>
- <!-- 로딩 상태 -->
- <div v-if="loading" class="loading-wrap">
- <v-progress-circular indeterminate color="primary"></v-progress-circular>
- <p>프로필을 불러오고 있습니다...</p>
- </div>
- <!-- 에러 상태 -->
- <div v-else-if="error" class="error-wrap">
- <v-alert type="error" dismissible @click:close="error = null">
- {{ error }}
- </v-alert>
- </div>
- <!-- 프로필 정보 -->
- <div v-else-if="profile" class="profile--wrap">
- <!-- 프로필 헤더 -->
- <div class="profile--header">
- <div class="profile--avatar">
- <v-img
- v-if="profile.PROFILE_IMAGE"
- :src="profile.PROFILE_IMAGE"
- :alt="profile.NICK_NAME + ' 프로필'"
- width="120"
- height="120"
- cover
- ></v-img>
- <div v-else class="no-avatar">
- {{ profile.NICK_NAME?.charAt(0) || "U" }}
- </div>
- </div>
- <div class="profile--info">
- <div class="profile--name">
- <h3>{{ profile.NICK_NAME }}</h3>
- <v-chip
- v-if="profile.PRIMARY_CATEGORY"
- color="primary"
- size="small"
- class="ml-2"
- >
- {{ getCategoryText(profile.PRIMARY_CATEGORY) }}
- </v-chip>
- </div>
- <div class="profile--meta">
- <div class="meta--item">
- <v-icon size="small">mdi-account-group</v-icon>
- <span>{{ formatNumber(profile.FOLLOWER_COUNT || 0) }} 팔로워</span>
- </div>
- <div class="meta--item">
- <v-icon size="small">mdi-chart-line</v-icon>
- <span>참여율 {{ profile.ENGAGEMENT_RATE || 0 }}%</span>
- </div>
- <div v-if="profile.REGION" class="meta--item">
- <v-icon size="small">mdi-map-marker</v-icon>
- <span>{{ profile.REGION }}</span>
- </div>
- </div>
- <p v-if="profile.DESCRIPTION" class="profile--description">
- {{ profile.DESCRIPTION }}
- </p>
- </div>
- </div>
- <!-- SNS 채널 -->
- <div v-if="snsChannels.length > 0" class="profile--channels">
- <h4>SNS 채널</h4>
- <div class="channels--grid">
- <a
- v-for="channel in snsChannels"
- :key="channel.platform"
- :href="getSnsUrl(channel)"
- target="_blank"
- rel="noopener noreferrer"
- class="channel--card"
- >
- <v-icon size="24" :color="getSnsColor(channel.platform)">
- {{ getSnsIcon(channel.platform) }}
- </v-icon>
- <div class="channel--info">
- <h5>{{ getSnsTitle(channel.platform) }}</h5>
- <p>{{ channel.handle }}</p>
- </div>
- <v-icon size="16">mdi-open-in-new</v-icon>
- </a>
- </div>
- </div>
- <!-- 콘텐츠 통계 -->
- <div class="profile--stats">
- <h4>콘텐츠 통계</h4>
- <div class="stats--grid">
- <div class="stat--card">
- <div class="stat--icon followers">
- <v-icon>mdi-account-group</v-icon>
- </div>
- <div class="stat--content">
- <h5>팔로워</h5>
- <p>{{ formatNumber(profile.FOLLOWER_COUNT || 0) }}</p>
- </div>
- </div>
- <div class="stat--card">
- <div class="stat--icon engagement">
- <v-icon>mdi-chart-line</v-icon>
- </div>
- <div class="stat--content">
- <h5>참여율</h5>
- <p>{{ profile.ENGAGEMENT_RATE || 0 }}%</p>
- </div>
- </div>
- <div class="stat--card">
- <div class="stat--icon partnerships">
- <v-icon>mdi-handshake</v-icon>
- </div>
- <div class="stat--content">
- <h5>협업</h5>
- <p>{{ formatNumber(partnershipCount || 0) }}건</p>
- </div>
- </div>
- </div>
- </div>
- <!-- 협업 이력 -->
- <div v-if="partnerships.length > 0" class="profile--partnerships">
- <h4>협업 이력</h4>
- <div class="partnerships--timeline">
- <div
- v-for="partnership in partnerships"
- :key="partnership.SEQ"
- class="timeline--item"
- >
- <div class="timeline--date">
- {{ formatDate(partnership.REG_DATE) }}
- </div>
- <div class="timeline--content">
- <h5>{{ partnership.vendorName }}</h5>
- <p v-if="partnership.DESCRIPTION">
- {{ partnership.DESCRIPTION }}
- </p>
- <div class="timeline--meta">
- <v-chip size="x-small" :color="getStatusColor(partnership.STATUS)">
- {{ getStatusText(partnership.STATUS) }}
- </v-chip>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 데이터 없음 -->
- <div v-else class="no-data-wrap">
- <div class="no-data">
- <v-icon size="64" color="grey-lighten-1">mdi-account-question</v-icon>
- <h3>프로필을 찾을 수 없습니다</h3>
- <p>요청하신 인플루언서 프로필이 존재하지 않습니다</p>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted } from "vue";
- import { useRoute } from "vue-router";
- /************************************************************************
- | 레이아웃
- ************************************************************************/
- definePageMeta({
- layout: "default",
- });
- /************************************************************************
- | 스토어 & 라우터
- ************************************************************************/
- const route = useRoute();
- const { $toast } = useNuxtApp();
- /************************************************************************
- | 반응형 데이터
- ************************************************************************/
- const loading = ref(false);
- const error = ref(null);
- const profile = ref(null);
- const partnerships = ref([]);
- const partnershipCount = ref(0);
- /************************************************************************
- | computed
- ************************************************************************/
- const snsChannels = computed(() => {
- if (!profile.value?.SNS_CHANNELS) return [];
- try {
- return JSON.parse(profile.value.SNS_CHANNELS);
- } catch (e) {
- return [];
- }
- });
- /************************************************************************
- | 메서드
- ************************************************************************/
- const loadProfile = async () => {
- try {
- loading.value = true;
- error.value = null;
- const influencerSeq = route.params.id;
- const params = { influencerSeq };
- useAxios()
- .post("/api/influencer/profile", params)
- .then((res) => {
- if (res.data.success) {
- profile.value = res.data.data.profile;
- partnerships.value = res.data.data.partnerships || [];
- partnershipCount.value = res.data.data.partnershipCount || 0;
- } else {
- error.value = res.data.message || "프로필을 불러오는데 실패했습니다.";
- }
- })
- .catch((err) => {
- error.value = err.message || "프로필을 불러오는데 실패했습니다.";
- })
- .finally(() => {
- loading.value = false;
- });
- } catch (err) {
- error.value = err.message || "프로필을 불러오는데 실패했습니다.";
- loading.value = false;
- }
- };
- const getCategoryText = (category) => {
- const categoryMap = {
- FASHION_BEAUTY: "패션·뷰티",
- FOOD_HEALTH: "식품·건강",
- LIFESTYLE: "라이프스타일",
- TECH_ELECTRONICS: "테크·가전",
- SPORTS_LEISURE: "스포츠·레저",
- CULTURE_ENTERTAINMENT: "문화·엔터테인먼트",
- };
- return categoryMap[category] || category || "기타";
- };
- const formatNumber = (num) => {
- if (!num) return "0";
- if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
- if (num >= 1000) return (num / 1000).toFixed(1) + "K";
- return num.toString();
- };
- const formatDate = (dateString) => {
- return new Date(dateString).toLocaleDateString("ko-KR");
- };
- const getSnsIcon = (platform) => {
- const iconMap = {
- instagram: "mdi-instagram",
- youtube: "mdi-youtube",
- tiktok: "mdi-music-note",
- blog: "mdi-post",
- facebook: "mdi-facebook",
- twitter: "mdi-twitter",
- };
- return iconMap[platform.toLowerCase()] || "mdi-link";
- };
- const getSnsColor = (platform) => {
- const colorMap = {
- instagram: "#E4405F",
- youtube: "#FF0000",
- tiktok: "#000000",
- blog: "#00B336",
- facebook: "#1877F2",
- twitter: "#1DA1F2",
- };
- return colorMap[platform.toLowerCase()] || "#666666";
- };
- const getSnsTitle = (platform) => {
- const titleMap = {
- instagram: "Instagram",
- youtube: "YouTube",
- tiktok: "TikTok",
- blog: "Blog",
- facebook: "Facebook",
- twitter: "Twitter",
- };
- return titleMap[platform] || platform;
- };
- const getSnsUrl = (channel) => {
- const handle = channel.handle.replace("@", "");
- const urlMap = {
- instagram: `https://instagram.com/${handle}`,
- youtube: `https://youtube.com/@${handle}`,
- tiktok: `https://tiktok.com/@${handle}`,
- blog: channel.handle.startsWith("http") ? channel.handle : `https://${handle}`,
- facebook: `https://facebook.com/${handle}`,
- twitter: `https://twitter.com/${handle}`,
- };
- return urlMap[channel.platform.toLowerCase()] || channel.handle;
- };
- const getStatusText = (status) => {
- const statusMap = {
- PENDING: "진행중",
- APPROVED: "완료",
- REJECTED: "거절됨",
- CANCELLED: "취소됨",
- };
- return statusMap[status] || status || "알 수 없음";
- };
- const getStatusColor = (status) => {
- const colorMap = {
- PENDING: "warning",
- APPROVED: "success",
- REJECTED: "error",
- CANCELLED: "grey",
- };
- return colorMap[status] || "grey";
- };
- /************************************************************************
- | 라이프사이클
- ************************************************************************/
- onMounted(() => {
- loadProfile();
- });
- </script>
- <style scoped>
- .profile--wrap {
- background: white;
- border-radius: 12px;
- padding: 24px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- .profile--header {
- display: flex;
- gap: 24px;
- margin-bottom: 32px;
- }
- .profile--avatar {
- width: 120px;
- height: 120px;
- border-radius: 60px;
- overflow: hidden;
- flex-shrink: 0;
- background: #f5f5f5;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .no-avatar {
- font-size: 48px;
- font-weight: bold;
- color: #666;
- }
- .profile--info {
- flex: 1;
- }
- .profile--name {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- }
- .profile--name h3 {
- margin: 0;
- font-size: 24px;
- font-weight: 600;
- }
- .profile--meta {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- margin-bottom: 16px;
- }
- .meta--item {
- display: flex;
- align-items: center;
- gap: 6px;
- color: #666;
- }
- .profile--description {
- font-size: 14px;
- line-height: 1.6;
- color: #444;
- margin: 0;
- }
- .profile--channels {
- margin-top: 32px;
- }
- .profile--channels h4,
- .profile--stats h4,
- .profile--partnerships h4 {
- margin: 0 0 16px 0;
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .channels--grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
- gap: 16px;
- }
- .channel--card {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 16px;
- background: #f8f9fa;
- border-radius: 8px;
- text-decoration: none;
- color: inherit;
- transition: transform 0.2s, box-shadow 0.2s;
- }
- .channel--card:hover {
- transform: translateY(-2px);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- .channel--info {
- flex: 1;
- }
- .channel--info h5 {
- margin: 0 0 4px 0;
- font-size: 14px;
- font-weight: 600;
- }
- .channel--info p {
- margin: 0;
- font-size: 13px;
- color: #666;
- }
- .profile--stats {
- margin-top: 32px;
- }
- .stats--grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 16px;
- }
- .stat--card {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 20px;
- background: #f8f9fa;
- border-radius: 8px;
- }
- .stat--icon {
- width: 48px;
- height: 48px;
- border-radius: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- }
- .stat--icon.followers {
- background: #2196f3;
- }
- .stat--icon.engagement {
- background: #4caf50;
- }
- .stat--icon.partnerships {
- background: #ff9800;
- }
- .stat--content h5 {
- margin: 0 0 4px 0;
- font-size: 14px;
- color: #666;
- }
- .stat--content p {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- color: #333;
- }
- .profile--partnerships {
- margin-top: 32px;
- }
- .partnerships--timeline {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .timeline--item {
- display: flex;
- gap: 16px;
- }
- .timeline--date {
- flex-shrink: 0;
- width: 100px;
- font-size: 14px;
- color: #666;
- }
- .timeline--content {
- flex: 1;
- background: #f8f9fa;
- padding: 16px;
- border-radius: 8px;
- position: relative;
- }
- .timeline--content::before {
- content: "";
- position: absolute;
- left: -8px;
- top: 50%;
- transform: translateY(-50%);
- width: 16px;
- height: 16px;
- background: #f8f9fa;
- transform: rotate(45deg);
- }
- .timeline--content h5 {
- margin: 0 0 8px 0;
- font-size: 16px;
- font-weight: 600;
- }
- .timeline--content p {
- margin: 0 0 8px 0;
- font-size: 14px;
- color: #666;
- }
- .timeline--meta {
- display: flex;
- gap: 8px;
- }
- .loading-wrap,
- .error-wrap,
- .no-data-wrap {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 20px;
- }
- .no-data {
- text-align: center;
- }
- .no-data h3 {
- margin: 16px 0 8px;
- color: #666;
- }
- .no-data p {
- color: #999;
- }
- @media (max-width: 768px) {
- .profile--header {
- flex-direction: column;
- align-items: center;
- text-align: center;
- }
- .profile--meta {
- justify-content: center;
- }
- .timeline--item {
- flex-direction: column;
- gap: 8px;
- }
- .timeline--date {
- width: auto;
- }
- .timeline--content::before {
- display: none;
- }
- }
- </style>
|